﻿using GodSharp.Communications.Abstractions;
using GodSharp.Sockets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;

namespace GodSharp.Communications.Socket
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="destination"></param>
    /// <param name="buffer"></param>
    /// <param name="sender"></param>
    internal delegate void TcpCommunicationDataEventHandler(string destination, byte[] buffer, ITcpConnection sender);

#pragma warning disable CS1584 // XML comment has syntactically incorrect cref attribute
#pragma warning disable CS1658 // Warning is overriding an error
    /// <summary>
    /// TcpCommunicationProvider
    /// </summary>
    /// <seealso cref="GodSharp.Communications.Abstractions.CommunicationProviderBase{GodSharp.Communications.Socket.TcpCommunicationProviderOptions}" />
    /// <seealso cref="GodSharp.Communications.Abstractions.ICommunicationProvider" />
    [NameDescription("TcpCommunicationProvider", "Tcp protocol for GodSharp.Communications")]
#pragma warning restore CS1658 // Warning is overriding an error
#pragma warning restore CS1584 // XML comment has syntactically incorrect cref attribute
    public sealed class TcpCommunicationProvider : CommunicationProviderBase<TcpCommunicationProviderOptions, ITcpCommunicationHandler>, ITcpCommunicationProvider
    {
        private Dictionary<string, TcpCommunicationDataEventHandler> OnData { get; set; }

        EventHandler<NetClientEventArgs<ITcpConnection>> OnServerClientConnected { get; set; }
        EventHandler<NetClientEventArgs<ITcpConnection>> OnServerClientDisconnected { get; set; }
        EventHandler<NetClientEventArgs<ITcpConnection>> OnClientConnected { get; set; }
        EventHandler<NetClientEventArgs<ITcpConnection>> OnClientDisconnected { get; set; }
        EventHandler<NetClientEventArgs<ITcpConnection>> OnClientException { get; set; }
        EventHandler<NetServerEventArgs> OnServerException { get; set; }

        Dictionary<int, TcpClient> clients = null;
        Dictionary<int, TcpServer> servers = null;
        SocketConfig[] sConfigs = null;
        SocketConfig[] scConfigs = null;
        SocketConfig[] cConfigs = null;

        Dictionary<int, ITcpConnection> senders = null;

        bool initialized = false;
        bool started = false;

        /// <summary>
        /// Initializes a new instance of the <see cref="TcpCommunicationProvider"/> class.
        /// </summary>
        public TcpCommunicationProvider()
        {
            this.OnData = new Dictionary<string, TcpCommunicationDataEventHandler>();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TcpCommunicationProvider"/> class.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        public TcpCommunicationProvider(TcpCommunicationProviderOptions parameter) : this()
        {
            Initialize(parameter);
        }

        /// <summary>
        /// Initializes the specified parameter.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException">
        /// parameter
        /// or
        /// ServerClientConfigs
        /// </exception>
        /// <exception cref="InvalidOperationException">The provider is initialized</exception>
        /// <exception cref="ArgumentException">
        /// Configs
        /// or
        /// Configs
        /// </exception>
        private bool Initialize(TcpCommunicationProviderOptions parameter)
        {
            if (parameter == null) throw new ArgumentNullException(nameof(parameter));

            OnServerClientConnected = parameter.ServerClientConnectedHandler;
            OnServerClientDisconnected = parameter.OnServerClientDisconnectedHandler;
            OnClientConnected = parameter.OnClientConnectedHandler;
            OnClientDisconnected = parameter.OnClientDisconnectedHandler;
            OnClientException = parameter.OnClientException;
            OnServerException = parameter.OnServerException;

            parameter.Configs = parameter.Configs?.Where(x => x.Available && x.ConfigType != SocketConfigFlags.UdpClient)?.ToArray();

            if (parameter == null || parameter.Configs.Count < 1) throw new ArgumentNullException(nameof(parameter));

            if (initialized) throw new InvalidOperationException("The provider is initialized");

            if (parameter.Configs.GroupBy(x => x.Id).Any(x => x.ToArray().Length > 1)) throw new ArgumentException($"Duplicate 'Id' at {nameof(parameter.Configs)}");

            SocketConfig[] configs = parameter.Configs.Where(x => x.Available).ToArray();

            sConfigs = configs.Where(x => x.ConfigType == SocketConfigFlags.TcpServer).ToArray();

            if (sConfigs.GroupBy(x => x.LocalPort).Any(x => x.ToArray().Length > 1)) throw new ArgumentException($"Duplicate 'Port' at {nameof(parameter.Configs)}");

            if (sConfigs?.Length > 0)
            {
                scConfigs = configs.Where(x => x.ConfigType == SocketConfigFlags.TcpServerClient).ToArray();

                if (scConfigs?.Length < 1) throw new ArgumentNullException(nameof(parameter.Configs));
            }

            cConfigs = configs.Where(x => x.ConfigType == SocketConfigFlags.TcpClient).ToArray();
            
            if (sConfigs?.Length > 0)
            {
                servers = new Dictionary<int, TcpServer>();

                foreach (var item in sConfigs)
                {
                    if (servers.ContainsKey(item.Id)) continue;

                    servers.Add(item.Id, new TcpServer(item.LocalPort.Value) { OnReceived = SocketServerDataHandler, OnConnected = SocketServerClientConnectedHandler, OnDisconnected = SocketServerClientDisconnectedHandler, OnException = SocketClientExceptionHandler, OnServerException = SocketServerExceptionHandler });
                }
            }

            if (cConfigs?.Length > 0)
            {
                clients = new Dictionary<int, TcpClient>();

                foreach (var item in cConfigs)
                {
                    if (clients.ContainsKey(item.Id)) continue;

                    clients.Add(item.Id, new TcpClient(item.Host, item.RemotePort.Value) { OnReceived = SocketClientDataHandler, OnConnected = SocketClientConnectedHandler, OnDisconnected = SocketClientDisconnectedHandler, OnException = SocketClientExceptionHandler });
                }
            }

            StateFlag = CommunicationModuleStateFlags.Initialized;

            initialized = true;

            return initialized;
        }

        /// <summary>
        /// Starts the specified objs.
        /// </summary>
        /// <param name="ids">The id array.</param>
        /// <returns></returns>
        public bool Start(params int[] ids)
        {
            CheckingState(true, false, true);

            StateFlag = CommunicationModuleStateFlags.Starting;

            bool error = false;

            List<int> sKeys = new List<int>();
            List<int> cKeys = new List<int>();

            if (ids?.Length > 0)
            {
                if (servers?.Count > 0) sKeys = servers.Keys.Where(x => ids.Contains(x)).ToList();
                if (clients?.Count > 0) cKeys = clients.Keys.Where(x => ids.Contains(x)).ToList();
            }
            else
            {
                sKeys = servers?.Keys.ToList();
                cKeys = clients?.Keys.ToList();
            }

            int counter = 0;

            if (sKeys?.Count > 0)
            {
                foreach (var key in sKeys)
                {
                    try
                    {
                        servers[key].Start();

                        if (!servers[key].Running)
                        {
                            error = true;
                            break;
                        }

                        counter++;
                    }
                    catch (Exception ex)
                    {
                        error = true;
                        Console.WriteLine(ex.Message);
                    }
                }
            }

            if (cKeys?.Count > 0)
            {
                foreach (var key in cKeys)
                {
                    try
                    {
                        clients[key].Start();
                        counter++;
                    }
                    catch (Exception ex)
                    {
                        error = true;
                        Console.WriteLine(ex.Message);
                    }
                }
            }

            senders = new Dictionary<int, ITcpConnection>();

            StateFlag = counter == 0 ? CommunicationModuleStateFlags.Initialized : (error ? CommunicationModuleStateFlags.StartWithError : CommunicationModuleStateFlags.Started);

            started = (StateFlag == CommunicationModuleStateFlags.Started || StateFlag == CommunicationModuleStateFlags.StartWithError);

            return started;
        }

        /// <summary>
        /// Stops the specified objs.
        /// </summary>
        /// <param name="ids">The id array.</param>
        /// <returns></returns>
        public bool Stop(params int[] ids)
        {
            CheckingState(true, true);

            started = false;

            if (servers?.Count > 0)
            {
                List<string> keys = new List<string>();

                foreach (var item in servers)
                {
                    try
                    {
                        if (!item.Value.Running) continue;

                        item.Value.Stop();
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                }
            }

            if (clients?.Count > 0)
            {
                List<string> keys = new List<string>();

                foreach (var item in clients)
                {
                    try
                    {
                        if (!item.Value.Running) continue;

                        item.Value.Stop();
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// Res the start.
        /// </summary>
        /// <param name="ids">The id array.</param>
        /// <returns></returns>
        public bool Restart(params int[] ids)
        {
            CheckingState(true, true);

            Stop();

            Start();

            return true;
        }

        /// <summary>
        /// Healthes the specified ids.
        /// </summary>
        /// <param name="ids">The ids.</param>
        /// <returns></returns>
        public IDictionary<int, CommunicationModuleStateFlags> Health(params int[] ids)
        {
            IDictionary<int, CommunicationModuleStateFlags> flags = null;

            if (servers?.Count > 0)
            {
                IDictionary<int, TcpServer> _servers = (ids.Length == 0 ? servers : servers.Where(x => ids.Contains(x.Key)).ToDictionary(x => x.Key, x => x.Value));

                if (_servers?.Count > 0)
                {
                    flags = new Dictionary<int, CommunicationModuleStateFlags>();

                    foreach (var item in _servers)
                    {
                        try
                        {
                            flags.Add(item.Key, !item.Value.Running ? CommunicationModuleStateFlags.Stopped :  CommunicationModuleStateFlags.Started);
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine(ex.Message);
                        }
                    }
                }
            }

            if (clients?.Count > 0)
            {
                IDictionary<int, TcpClient> _clients = (ids.Length == 0 ? clients : clients.Where(x => ids.Contains(x.Key)).ToDictionary(x => x.Key, x => x.Value));

                if (_clients?.Count > 0)
                {
                    flags = new Dictionary<int, CommunicationModuleStateFlags>();

                    foreach (var item in _clients)
                    {
                        try
                        {
                            flags.Add(item.Key, item.Value.Running ? CommunicationModuleStateFlags.Started : CommunicationModuleStateFlags.Stopped);
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine(ex.Message);
                        }
                    }
                }
            }

            return flags;
        }

        /// <summary>
        /// Sockets the server data handler.
        /// </summary>
        /// <param name="args">The <see cref="NetClientReceivedEventArgs{ITcpConnection}"/> instance containing the event data.</param>
        private void SocketServerDataHandler(NetClientReceivedEventArgs<ITcpConnection> args)
        {
            try
            {
                Console.WriteLine($"Tcp:{args.NetConnection.Key} Received from client {args.RemoteEndPoint}");
                
                SocketConfig config = scConfigs.FirstOrDefault(x => x.Host == args.RemoteEndPoint.Address.ToString() && (x.RemotePort == 0 || x.RemotePort == args.RemoteEndPoint.Port));

                SocketDataHandler(config, args.NetConnection, args.Buffers);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        /// <summary>
        /// Sockets the client data handler.
        /// </summary>
        /// <param name="args">The <see cref="NetClientReceivedEventArgs{ITcpConnection}"/> instance containing the event data.</param>
        private void SocketClientDataHandler(NetClientReceivedEventArgs<ITcpConnection> args)
        {
            try
            {
                Console.WriteLine($"Tcp:{args.NetConnection.Key} Received from server {args.RemoteEndPoint}");

                SocketConfig config = cConfigs.FirstOrDefault(x => x.Host == args.RemoteEndPoint.Address.ToString() && x.RemotePort == args.RemoteEndPoint.Port);

                SocketDataHandler(config, args.NetConnection, args.Buffers);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        /// <summary>
        /// Sockets the data handler.
        /// </summary>
        /// <param name="config">The configuration.</param>
        /// <param name="client">The client.</param>
        /// <param name="buffer">The buffer.</param>
        private void SocketDataHandler(SocketConfig config, ITcpConnection client, byte[] buffer)
        {
            if (config == null) return;

            OnData[config.EntryPoint]?.Invoke(config.EntryPoint, buffer, client);
        }

        /// <summary>
        /// Sockets the server client connected handler.
        /// </summary>
        /// <param name="args">The <see cref="NetClientEventArgs{ITcpConnection}"/> instance containing the event data.</param>
        private void SocketServerClientConnectedHandler(NetClientEventArgs<ITcpConnection> args)
        {
            Console.WriteLine($"tcp.client [{args.RemoteEndPoint}] connected to tcp.server [{args.LocalEndPoint}]");

            SocketConfig config = GetSocketConfig(args.NetConnection, SocketTypeFlags.TcpServer);

            if (config != null)
            {
                RemoveSender(config.Id);

                senders.Add(config.Id, args.NetConnection);
            }

            OnServerClientConnected?.Invoke(args);
        }

        /// <summary>
        /// Sockets the server client disconnected handler.
        /// </summary>
        /// <param name="args">The <see cref="NetClientEventArgs{ITcpConnection}"/> instance containing the event data.</param>
        private void SocketServerClientDisconnectedHandler(NetClientEventArgs<ITcpConnection> args)
        {
            Console.WriteLine($"tcp.client [{args.RemoteEndPoint}] disconnected from tcp.server [{args.LocalEndPoint}]");

            SocketConfig config = GetSocketConfig(args.NetConnection, SocketTypeFlags.TcpServer);

            if (config != null) RemoveSender(config.Id);

            OnServerClientDisconnected?.Invoke(args);
        }

        /// <summary>
        /// Sockets the client connected handler.
        /// </summary>
        /// <param name="args">The <see cref="NetClientEventArgs{ITcpConnection}"/> instance containing the event data.</param>
        private void SocketClientConnectedHandler(NetClientEventArgs<ITcpConnection> args)
        {
            Console.WriteLine($"tcp.client [{args.LocalEndPoint}] connected to tcp.server [{args.RemoteEndPoint}]");

            SocketConfig config = GetSocketConfig(args.NetConnection, SocketTypeFlags.TcpClient);

            if (config != null)
            {
                if (senders == null) senders = new Dictionary<int, ITcpConnection>();

                if (senders.ContainsKey(config.Id)) RemoveSender(config.Id);

                senders.Add(config.Id, args.NetConnection);
            }

            OnClientConnected?.Invoke(args);
        }

        /// <summary>
        /// Sockets the client disconnected handler.
        /// </summary>
        /// <param name="args">The <see cref="NetClientEventArgs{ITcpConnection}"/> instance containing the event data.</param>
        private void SocketClientDisconnectedHandler(NetClientEventArgs<ITcpConnection> args)
        {
            Console.WriteLine($"tcp.client [{args.LocalEndPoint}] disconnected from tcp.server [{args.RemoteEndPoint}]");

            SocketConfig config = GetSocketConfig(args.NetConnection, SocketTypeFlags.TcpClient);

            if (config != null) RemoveSender(config.Id);

            OnClientDisconnected(args);
        }
        
        /// <summary>
        /// Sockets the client exception handler.
        /// </summary>
        /// <param name="args">The <see cref="NetClientEventArgs{ITcpConnection}"/> instance containing the event data.</param>
        private void SocketClientExceptionHandler(NetClientEventArgs<ITcpConnection> args)
        {
            Console.WriteLine($"tcp.client [{args.RemoteEndPoint}] throw exception:{args.Exception?.Message}");

            OnClientException?.Invoke(args);
        }

        /// <summary>
        /// Sockets the server exception handler.
        /// </summary>
        /// <param name="args">The <see cref="NetServerEventArgs"/> instance containing the event data.</param>
        private void SocketServerExceptionHandler(NetServerEventArgs args)
        {
            Console.WriteLine($"tcp.server [{args.LocalEndPoint}] throw exception:{args.Exception?.Message}");

            OnServerException?.Invoke(args);
        }

        /// <summary>
        /// Gets the socket configuration.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="flags">The flags.</param>
        /// <returns></returns>
        private SocketConfig GetSocketConfig(ITcpConnection sender,SocketTypeFlags flags)
        {
            SocketConfig config = null;
            IPEndPoint point = sender.RemoteEndPoint as IPEndPoint;

            switch (flags)
            {
                case SocketTypeFlags.TcpClient:
                    config = cConfigs.FirstOrDefault(x => x.Host == point.Address.ToString() && x.RemotePort == point.Port);
                    break;
                case SocketTypeFlags.TcpServer:
                    config = scConfigs.FirstOrDefault(x => x.Host == point.Address.ToString() && (x.RemotePort == 0 || x.RemotePort == point.Port));
                    break;
                default:
                    break;
            }

            return config;
        }

        /// <summary>
        /// Removes the sender.
        /// </summary>
        /// <param name="id">The identifier.</param>
        private void RemoveSender(int id)
        {
            if (senders.ContainsKey(id)) senders.Remove(id);
        }

        /// <summary>
        /// Checkings the state.
        /// </summary>
        /// <param name="initialized">if set to <c>true</c> [initialized].</param>
        /// <param name="started">if set to <c>true</c> [started].</param>
        /// <param name="restarting">if set to <c>true</c> [restarting].</param>
        /// <exception cref="System.InvalidOperationException">
        /// The module is not initialized
        /// or
        /// The module is not started
        /// or
        /// The module is running
        /// </exception>
        private void CheckingState(bool initialized = false, bool started = false, bool restarting = false)
        {
            if (initialized && !this.initialized) throw new InvalidOperationException("The provider is not initialized");

            if (started && !this.started) throw new InvalidOperationException("The provider is not started");

            if (restarting && this.started) throw new InvalidOperationException("The provider is running");
        }

        /// <summary>
        /// Registers the modules.
        /// </summary>
        /// <param name="types">The types.</param>
        public void RegisterModules(Type[] types)
        {
            OnRegisterModules(types, (entry, handler) =>
            {
                if (!this.OnData.ContainsKey(entry)) this.OnData.Add(entry, handler.OnTcpDataHandler);
                else this.OnData[entry] += handler.OnTcpDataHandler;
            });
        }

        /// <summary>
        /// Gets the TCP sender.
        /// </summary>
        /// <param name="id">The identifier.</param>
        /// <returns></returns>
        public ITcpConnection GetTcpConnection(int id)
        {
            if (senders.ContainsKey(id)) return senders[id];

            return null;
        }
    }
}